/*
 * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.engine.test.api.runtime;

import static java.util.Arrays.asList;
import static org.activiti.engine.impl.util.CollectionUtil.map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.impl.test.PluggableActivitiTestCase;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.Execution;
import org.activiti.engine.runtime.Job;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.IdentityLinkType;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.Deployment;

public class ProcessInstanceSuspensionTest extends PluggableActivitiTestCase {

    @Deployment(resources = { "org/activiti/engine/test/api/runtime/oneTaskProcess.bpmn20.xml" })
    public void testProcessInstanceActiveByDefault() {
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        runtimeService.startProcessInstanceByKey(processDefinition.getKey());

        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
        assertThat(processInstance.isSuspended()).isFalse();
    }

    @Deployment(resources = { "org/activiti/engine/test/api/runtime/oneTaskProcess.bpmn20.xml" })
    public void testSuspendActivateProcessInstance() {
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        runtimeService.startProcessInstanceByKey(processDefinition.getKey());

        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
        assertThat(processInstance.isSuspended()).isFalse();

        // suspend
        runtimeService.suspendProcessInstanceById(processInstance.getId());
        processInstance = runtimeService.createProcessInstanceQuery().singleResult();
        assertThat(processInstance.isSuspended()).isTrue();

        // activate
        runtimeService.activateProcessInstanceById(processInstance.getId());
        processInstance = runtimeService.createProcessInstanceQuery().singleResult();
        assertThat(processInstance.isSuspended()).isFalse();
    }

    @Deployment(resources = { "org/activiti/engine/test/api/runtime/oneTaskProcess.bpmn20.xml" })
    public void testCannotActivateActiveProcessInstance() {
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        runtimeService.startProcessInstanceByKey(processDefinition.getKey());

        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
        assertThat(processInstance.isSuspended()).isFalse();

        assertThatExceptionOfType(ActivitiException.class).isThrownBy(() ->
            runtimeService.activateProcessInstanceById(processInstance.getId())
        );
    }

    @Deployment(resources = { "org/activiti/engine/test/api/runtime/oneTaskProcess.bpmn20.xml" })
    public void testCannotSuspendSuspendedProcessInstance() {
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        runtimeService.startProcessInstanceByKey(processDefinition.getKey());

        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
        assertThat(processInstance.isSuspended()).isFalse();

        runtimeService.suspendProcessInstanceById(processInstance.getId());

        assertThatExceptionOfType(ActivitiException.class).isThrownBy(() ->
            runtimeService.suspendProcessInstanceById(processInstance.getId())
        );
    }

    @Deployment(
        resources = {
            "org/activiti/engine/test/api/runtime/superProcessWithMultipleNestedSubProcess.bpmn20.xml",
            "org/activiti/engine/test/api/runtime/nestedSubProcess.bpmn20.xml",
            "org/activiti/engine/test/api/runtime/subProcess.bpmn20.xml",
        }
    )
    public void testQueryForActiveAndSuspendedProcessInstances() {
        runtimeService.startProcessInstanceByKey("nestedSubProcessQueryTest");

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(5);
        assertThat(runtimeService.createProcessInstanceQuery().active().count()).isEqualTo(5);
        assertThat(runtimeService.createProcessInstanceQuery().suspended().count()).isEqualTo(0);

        ProcessInstance piToSuspend = runtimeService
            .createProcessInstanceQuery()
            .processDefinitionKey("nestedSubProcessQueryTest")
            .singleResult();
        runtimeService.suspendProcessInstanceById(piToSuspend.getId());

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(5);
        assertThat(runtimeService.createProcessInstanceQuery().active().count()).isEqualTo(4);
        assertThat(runtimeService.createProcessInstanceQuery().suspended().count()).isEqualTo(1);

        assertThat(runtimeService.createProcessInstanceQuery().suspended().singleResult().getId()).isEqualTo(
            piToSuspend.getId()
        );
    }

    @Deployment(resources = { "org/activiti/engine/test/api/runtime/oneTaskProcess.bpmn20.xml" })
    public void testTaskSuspendedAfterProcessInstanceSuspension() {
        // Start Process Instance
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        runtimeService.startProcessInstanceByKey(processDefinition.getKey());
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();

        // Suspense process instance
        runtimeService.suspendProcessInstanceById(processInstance.getId());

        // Assert that the task is now also suspended
        List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
        for (Task task : tasks) {
            assertThat(task.isSuspended()).isTrue();
        }

        // Activate process instance again
        runtimeService.activateProcessInstanceById(processInstance.getId());
        tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
        for (Task task : tasks) {
            assertThat(task.isSuspended()).isFalse();
        }
    }

    @Deployment(resources = { "org/activiti/engine/test/api/oneTaskProcess.bpmn20.xml" })
    public void testTaskQueryAfterProcessInstanceSuspend() {
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId());

        Task task = taskService.createTaskQuery().singleResult();
        assertThat(task).isNotNull();

        task = taskService.createTaskQuery().active().singleResult();
        assertThat(task).isNotNull();

        // Suspend
        runtimeService.suspendProcessInstanceById(processInstance.getId());
        assertThat(taskService.createTaskQuery().count()).isEqualTo(1);
        assertThat(taskService.createTaskQuery().suspended().count()).isEqualTo(1);
        assertThat(taskService.createTaskQuery().active().count()).isEqualTo(0);

        // Activate
        runtimeService.activateProcessInstanceById(processInstance.getId());
        assertThat(taskService.createTaskQuery().count()).isEqualTo(1);
        assertThat(taskService.createTaskQuery().suspended().count()).isEqualTo(0);
        assertThat(taskService.createTaskQuery().active().count()).isEqualTo(1);

        // Completing should end the process instance
        task = taskService.createTaskQuery().singleResult();
        taskService.complete(task.getId());
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
    }

    @Deployment
    public void testChildExecutionsSuspendedAfterProcessInstanceSuspend() {
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testChildExecutionsSuspended");
        runtimeService.suspendProcessInstanceById(processInstance.getId());

        List<Execution> executions = runtimeService
            .createExecutionQuery()
            .processInstanceId(processInstance.getId())
            .list();
        for (Execution execution : executions) {
            assertThat(execution.isSuspended()).isTrue();
        }

        // Activate again
        runtimeService.activateProcessInstanceById(processInstance.getId());
        executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).list();
        for (Execution execution : executions) {
            assertThat(execution.isSuspended()).isFalse();
        }

        // Finish process
        while (taskService.createTaskQuery().count() > 0) {
            for (Task task : taskService.createTaskQuery().list()) {
                taskService.complete(task.getId());
            }
        }
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
    }

    @Deployment(resources = { "org/activiti/engine/test/api/oneTaskProcess.bpmn20.xml" })
    public void testProcessInstanceOperationsFailAfterSuspend() {
        // Suspend process instance
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId());
        runtimeService.suspendProcessInstanceById(processInstance.getId());

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.messageEventReceived("someMessage", processInstance.getId()))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() ->
                runtimeService.messageEventReceived("someMessage", processInstance.getId(), new HashMap<>())
            )
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.removeVariable(processInstance.getId(), "someVariable"))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.removeVariableLocal(processInstance.getId(), "someVariable"))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.removeVariables(processInstance.getId(), asList("one", "two", "three")))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() ->
                runtimeService.removeVariablesLocal(processInstance.getId(), asList("one", "two", "three"))
            )
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.setVariable(processInstance.getId(), "someVariable", "someValue"))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.setVariableLocal(processInstance.getId(), "someVariable", "someValue"))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.setVariables(processInstance.getId(), new HashMap<>()))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.setVariablesLocal(processInstance.getId(), new HashMap<>()))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.trigger(processInstance.getId()))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.trigger(processInstance.getId(), new HashMap<>()))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() -> runtimeService.signalEventReceived("someSignal", processInstance.getId()))
            .withMessageContaining("is suspended");

        assertThatExceptionOfType(ActivitiException.class)
            .isThrownBy(() ->
                runtimeService.signalEventReceived("someSignal", processInstance.getId(), new HashMap<>())
            )
            .withMessageContaining("is suspended");
    }

    @Deployment
    public void testSignalEventReceivedAfterProcessInstanceSuspended() {
        final String signal = "Some Signal";

        // Test if process instance can be completed using the signal
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("signalSuspendedProcessInstance");
        runtimeService.signalEventReceived(signal);
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);

        // Now test when suspending the process instance: the process instance
        // shouldn't be continued
        processInstance = runtimeService.startProcessInstanceByKey("signalSuspendedProcessInstance");
        runtimeService.suspendProcessInstanceById(processInstance.getId());
        runtimeService.signalEventReceived(signal);
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(1);

        runtimeService.signalEventReceived(signal, new HashMap<String, Object>());
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(1);

        // Activate and try again
        runtimeService.activateProcessInstanceById(processInstance.getId());
        runtimeService.signalEventReceived(signal);
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
    }

    @Deployment(
        resources = "org/activiti/engine/test/api/runtime/ProcessInstanceSuspensionTest.testSignalEventReceivedAfterProcessInstanceSuspended.bpmn20.xml"
    )
    public void testSignalEventReceivedAfterMultipleProcessInstancesSuspended() {
        final String signal = "Some Signal";

        // Test if process instance can be completed using the signal
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("signalSuspendedProcessInstance");
        runtimeService.startProcessInstanceByKey("signalSuspendedProcessInstance");
        runtimeService.signalEventReceived(signal);
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);

        // Now test when suspending the process instance: the process instance
        // shouldn't be continued
        processInstance = runtimeService.startProcessInstanceByKey("signalSuspendedProcessInstance");
        runtimeService.suspendProcessInstanceById(processInstance.getId());
        processInstance = runtimeService.startProcessInstanceByKey("signalSuspendedProcessInstance");
        runtimeService.suspendProcessInstanceById(processInstance.getId());
        runtimeService.signalEventReceived(signal);
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(2);

        runtimeService.signalEventReceived(signal, new HashMap<String, Object>());
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(2);

        // Activate and try again
        runtimeService.activateProcessInstanceById(processInstance.getId());
        runtimeService.signalEventReceived(signal);
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(1);
    }

    @Deployment(resources = { "org/activiti/engine/test/api/oneTaskProcess.bpmn20.xml" })
    public void testTaskOperationsFailAfterProcessInstanceSuspend() {
        // Start a new process instance with one task
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId());
        final Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
        assertThat(task).isNotNull();

        // Suspend the process instance
        runtimeService.suspendProcessInstanceById(processInstance.getId());

        // Yeah, the following is pretty long and boring ... but I didn't have
        // the patience
        // to create separate tests for each of them.

        // Completing the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to complete a task of a suspended process instance")
            .isThrownBy(() -> taskService.complete(task.getId()));

        // Claiming the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to claim a task of a suspended process instance")
            .isThrownBy(() -> taskService.claim(task.getId(), "jos"));

        // Setting variable on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to set a variable on a task of a suspended process instance")
            .isThrownBy(() -> taskService.setVariable(task.getId(), "someVar", "someValue"));

        // Setting variable on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to set a variable on a task of a suspended process instance")
            .isThrownBy(() -> taskService.setVariableLocal(task.getId(), "someVar", "someValue"));

        // Setting variables on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to set variables on a task of a suspended process instance")
            .isThrownBy(() -> taskService.setVariables(task.getId(), map("varOne", "one", "varTwo", "two")));

        // Setting variables on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to set variables on a task of a suspended process instance")
            .isThrownBy(() -> taskService.setVariablesLocal(task.getId(), map("varOne", "one", "varTwo", "two")));

        // Removing variable on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to remove a variable on a task of a suspended process instance")
            .isThrownBy(() -> taskService.removeVariable(task.getId(), "someVar"));

        // Removing variable on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to remove a variable on a task of a suspended process instance")
            .isThrownBy(() -> taskService.removeVariableLocal(task.getId(), "someVar"));

        // Removing variables on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to remove variables on a task of a suspended process instance")
            .isThrownBy(() -> taskService.removeVariables(task.getId(), asList("one", "two")));

        // Removing variables on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to remove variables on a task of a suspended process instance")
            .isThrownBy(() -> taskService.removeVariablesLocal(task.getId(), asList("one", "two")));

        // Adding candidate groups on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to add a candidate group on a task of a suspended process instance")
            .isThrownBy(() -> taskService.addCandidateGroup(task.getId(), "blahGroup"));

        // Adding candidate users on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to add a candidate user on a task of a suspended process instance")
            .isThrownBy(() -> taskService.addCandidateUser(task.getId(), "blahUser"));

        // Adding candidate users on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to add a candidate user on a task of a suspended process instance")
            .isThrownBy(() -> taskService.addGroupIdentityLink(task.getId(), "blahGroup", IdentityLinkType.CANDIDATE));

        // Adding an identity link on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to add an identityLink on a task of a suspended process instance")
            .isThrownBy(() -> taskService.addUserIdentityLink(task.getId(), "blahUser", IdentityLinkType.OWNER));

        // Adding a comment on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to add a comment on a task of a suspended process instance")
            .isThrownBy(() -> taskService.addComment(task.getId(), processInstance.getId(), "test comment"));

        // Adding an attachment on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to add an attachment on a task of a suspended process instance")
            .isThrownBy(() ->
                taskService.createAttachment(
                    "text",
                    task.getId(),
                    processInstance.getId(),
                    "testName",
                    "testDescription",
                    "http://test.com"
                )
            );

        // Set an assignee on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to set an assignee on a task of a suspended process instance")
            .isThrownBy(() -> taskService.setAssignee(task.getId(), "mispiggy"));

        // Set an owner on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to set an owner on a task of a suspended process instance")
            .isThrownBy(() -> taskService.setOwner(task.getId(), "kermit"));

        // Set priority on the task should fail
        assertThatExceptionOfType(ActivitiException.class)
            .as("It is not allowed to set a priority on a task of a suspended process instance")
            .isThrownBy(() -> taskService.setPriority(task.getId(), 99));
    }

    @Deployment
    public void testJobNotExecutedAfterProcessInstanceSuspend() {
        Date now = new Date();
        processEngineConfiguration.getClock().setCurrentTime(now);

        // Suspending the process instance should also stop the execution of jobs for that process instance
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
        ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId());
        assertThat(managementService.createTimerJobQuery().count()).isEqualTo(1);
        runtimeService.suspendProcessInstanceById(processInstance.getId());
        assertThat(managementService.createSuspendedJobQuery().count()).isEqualTo(1);

        // The jobs should not be executed now
        processEngineConfiguration.getClock().setCurrentTime(new Date(now.getTime() + (60 * 60 * 1000))); // Timer is set to fire on 5 minutes
        Job job = managementService.createTimerJobQuery().executable().singleResult();
        assertThat(job).isNull();

        assertThat(managementService.createSuspendedJobQuery().count()).isEqualTo(1);

        // Activation of the process instance should now allow for job execution
        runtimeService.activateProcessInstanceById(processInstance.getId());
        waitForJobExecutorToProcessAllJobs(10000L, 100L);
        assertThat(managementService.createJobQuery().count()).isEqualTo(0);
        assertThat(managementService.createTimerJobQuery().count()).isEqualTo(0);
        assertThat(managementService.createSuspendedJobQuery().count()).isEqualTo(0);
        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
    }
}
